租约和 fencing:把旧持有者挡在资源提交处
要保护的对象是“某份结果不能被过期工作覆盖”,不是锁键本身。假设两个非恶意 worker 会暂停、恢复,网络会延迟或丢响应;授权服务与资源各自有持久状态,二者不共享事务。本文给出通用租约协议的唯一权威讲解;具体 Redis/ZooKeeper 配方只说明如何实现其前提。
owner、租期与资源授权是三件事
随机 owner 标识本次申请,释放或续租时原子比较 owner,能防止 A 删除或延长 B 的锁。它不能阻止旧 A 写数据库:A 在续租前、检查后或发送请求后都可能暂停,甚至写请求本身已在网络中等待。租约失效改变授权服务状态,不会撤销远端指令,也不会杀死线程。
租期依赖服务端过期、时钟/漂移界限、续租与故障检测规则;“本地还剩 5 秒”不是跨进程事实。把暂停恢复误当 crash-stop,会漏掉最危险的旧持有者。若业务必须在某个墙钟时刻后拒绝所有旧请求,资源必须参与验证有效期/撤权,并证明时钟和通信前提;仅比较 fencing token 无此承诺。
41、42、41:提交时间线
- t0:A 从同一授权权威领取
(owner=A, fence=41),开始计算。 - t5:A 暂停;t10:授权权威判其到期,把新租约和
42原子授给 B。 - t11:B 带 42 发布结果,资源持久保存
max_fence=42。 - t20:A 恢复,带 41 发布。资源在真正修改业务状态的原子边界拒绝它;调用前自查不能替代此处检查。
关键限定:若 A 的 41 在资源持久接受 B 的 42 新状态前提交,资源仍可能接受。资源只知道已持久接受业务写入的最大代际,不知道授权服务此刻的租期。fencing 保证新代际业务写入已被资源持久接受后,旧代际不能再覆盖它;不按租约墙钟倒推此前写入非法。
发号必须与授予同属一个原子权威
每个资源授权域维护持久的 (owner, expiry, generation);成功 acquire 在一个串行裁决中判断可授予、递增 generation、写入新 owner/expiry,并返回绑定该授权的 token。续租只允许仍匹配的 owner;续租失败后不能独立申请更大 token 来“补救”。这是协议伪代码,不是某组件的原码:
atomic authority.acquire(resource, request_id, owner):
若 request_id 已成功:返回原结果,而不是再次递增
若当前租约仍有效:拒绝或等待
generation := durable_generation + 1
持久提交 (generation, owner, expiry, request_id)
返回绑定本次授予的 generation反例:A 先判断自己持锁,然后暂停;B 获授权并取 42、提交;A 恢复后才调用独立自增服务取得 43。资源即使正确拒绝小 token,也会接受未绑定有效授予的 43。问题是授予协议,换成更大的整数不能修复。资源还需认证调用者与授权域;fence 不是防恶意伪造的凭据。
跨故障单调性同样属于授权权威:若从旧备份恢复发号器至 40,B 可能再次获得 41。资源无法区分两个 41 所属的任期,旧 A 的较大 op_seq 甚至可能覆盖 B。可选方案是可靠持久高水位、保证不重用的新 epoch 加计数器,或先封停旧权威/旧资源通道再受控切换授权域;新 epoch 自身也必须有不回退的来源与可比较规则。只读某一个资源的 max 不能证明全域没有更大 token,Redis INCR 的异步故障切换也不天然满足此要求。
一个任期多次写:fence、op_seq 与业务身份
最简单的 WHERE max_fence < incoming_fence 每个任期只允许第一次写。改成 <= 会允许同任期请求重复执行;两者都不是完整的多操作协议。
对一个串行发布流,为本任期分配递增的 op_seq,资源原子比较 (fence, op_seq) 的字典序。若 seq2 先到,seq1 后到应被拒;因此它适合“新状态覆盖旧状态”。若业务要求每个增量都执行,必须检测缺口并排序/重试,不能把丢失 seq1 叫成功。每个逻辑业务操作另有稳定幂等键及请求摘要,跨任期重试仍用同一键,attempt/owner/token 不代替业务身份。
以下是教学事务与条件更新,未声称在本轮执行过 MySQL:
BEGIN;
-- 对同一业务键先用唯一约束占位,或读已提交结果并比较摘要;
-- 冲突则返回参数冲突,同键同摘要则返回原结果,不重复副作用。
UPDATE resource
SET value = :value, max_fence = :fence, last_seq = :seq
WHERE id = :id
AND (max_fence < :fence OR (max_fence = :fence AND last_seq < :seq));
-- 必须检查影响行数;0 表示未获本次提交权,不继续业务写。
-- 业务效果、幂等记录和结果都在此事务内提交;失败一同回滚。
COMMIT;原子边界包含“检查代际/序号、检查业务身份、业务效果、记录结果”,不能 SELECT 检查后离开事务再 UPDATE。唯一键冲突需按实际数据库隔离与并发规则读原结果;同键异参必须报冲突,不能悄悄返回别人的成功。去重记录寿命覆盖客户端重试、消息重放、灾备与人工补单窗口;过早删记录会重新开放旧业务操作。
同键同摘要返回旧结果属于只读查询,不表示携带的新任期已提交一份新状态。本文 operations 模型不因此推进 fence;若协议要求任何新任期请求(包括查询原结果)都建立新高水位,应将该推进也纳入资源事务,并调整可接受历史。
资源参与方式与不能保护的效果
| 资源条件 | 可实现的提交点 | 失败边界 |
|---|---|---|
| 一个数据库内的状态与账本 | 条件更新、唯一约束和业务写同事务 | 数据库外的调用不因该事务回滚 |
| 大对象可不可变写,指针支持 CAS | 先写唯一对象,再用带 fence/seq 的原子 CAS 发布指针 | 旧 worker 可留下未引用对象,要回收;读者只认已发布指针 |
| 邮件/支付 API 不理解 token | 不能声称其受此 fence 保护 | 发送已发生时后续拒绝旧数据库写也无法撤回;需服务方幂等、状态查询及恢复协议 |
若资源自身已有串行事务裁决,方案 A 直接用资源版本/状态机通常较少协调环节;方案 B 先租约领取长工作再做资源 fence,可以降低重复计算但增加授权恢复和两端未知结果。长计算或锁持有时间短都不是独立使用外部租约的充分理由。对不可参与的副作用,改推荐业务协议,不能靠缩短租期许诺“恰好一次”。
Broker 会话去重、Kafka 事务的输出与位点、数据库本地事务分别有各自原子域,均不会自动覆盖任意外部 API。UNKNOWN 的历史处理见状态与操作;端到端双写、Inbox/Outbox 与结果查询的当前权威在Outbox和跨资源事务,本页不重复维护其协议。
可运行实验:事件模型与三种故意缺陷
输入为两客户端 A/B、一个授权域、一个资源,初始 issuer=40,租约到期由离散事件表示。执行完整依赖、下载及命令:
python3 labs/middleware-senior/reliability_lab.py fencing
python3 labs/middleware-senior/reliability_lab.py fencing --variant no-fence
python3 labs/middleware-senior/reliability_lab.py fencing --variant split-authority
python3 labs/middleware-senior/reliability_lab.py fencing --variant rollback-issuer
python3 labs/middleware-senior/reliability_lab.py fencing --case operations步骤与预期:先枚举 acquire A→暂停→到期→持久恢复→acquire B 之后的全部三种写入/恢复交错。正确版本无越代覆盖;A 可先于 B 提交,但 B 先提交后 A 必须被拒。然后依次关闭资源检查、拆开发号授权、恢复旧 issuer,检查输出的 counterexample.events 与每步资源值;三缺陷分别出现 revoked_generation_overwrite、unauthorized_accept、revoked_generation_overwrite。拆分版本因额外 mint 依赖有两条交错,旧 A 最后拿到 43。最后 operations 输入 seq1/seq2、跨任期同键重试、同键异参、新键旧序号,预期 applied/applied/duplicate/conflict/stale,只有两个效果。
本轮已运行以上模型与公开 CLI 测试。每个授权授予和每个资源事务各是一个不可分割模型转换,二者之间可交错;模型检查的性质是已提交资源代际不后退,以及未绑定授予的 token 不可产生效果。只枚举这里声明的有限偏序,不证明任意客户端、任意长历史、时钟误差或存储故障。它没有真实网络、ZooKeeper/Raft 集群、掉电、数据库隔离或外部 API;本机 Redis standalone 也不能替代共识试验。真实实现还需对照这些原子边界注入暂停、丢响应、持久恢复与多节点切换。
U60:真实 Redis 授权权威 + 真实 MySQL 资源的验证
上文事件模型是纸面证据;U60 补了一次真实服务验证:授权权威是本机 Redis 8.6.4 standalone(Lua 原子授予:generation 独立持久键 INCR、租期 PEXPIRE、owner 校验续租),资源是本机 MySQL 8.4.7((max_fence,last_seq) 字典序条件更新与业务写同事务)。实验按 41/42/41 时间线实跑:A 取 fence=1、暂停到期、B 取 fence=2 并提交资源、A 的过期 fence=1 被资源拒绝;同任期 seq2 先到拒绝迟到 seq1;同键同摘要返回原结果、异参报冲突。入口见 labs/correctness-u60。
实验同时暴露了两个实现级事实,与上文协议互为印证:发号的持久状态不能放在会过期的租约键里(第一版把 generation 存进租约哈希,租约过期后 generation 归零,B 拿到与 A 相同的 1——正是"持久高水位与租期状态必须分开"的反例);MANUAL 式逐条确认在 MySQL 侧不存在,条件更新的幂等摘要必须与原执行载荷一致,否则同键重试会被误判为冲突。边界:单机 Redis 是唯一权威,其自身故障即权威不可用(活性边界);未运行跨主机网络故障、多权威共识与时钟注入。
三轮追问与条件变化
第一问:既然能 fence,为何还要 owner?答:owner 防续租/释放误操作,fence 保护参与的业务资源,两个操作域不同。
第二问:B 已获 42,A 在资源提交 42 新状态前写入是否违规?答:对上述 fence 性质不违规;若业务要求实时撤权,应更改资源验证协议与性质,不能给原模型加一句“到期立即无效”。
第三问:把数据库发布换成第三方扣款,应推荐哪个候选?答:资源不参与 CAS 时原 fence 方案不足,应围绕商户业务键、可查询结果和 UNKNOWN 恢复设计;若服务方连幂等/查询都不提供,必须暴露人工裁决和重复风险,不能承诺自动安全重试。
闭卷验收
本页 4 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与验证边界
- Chubby 论文 §2.1/§2.4:租约、客户端暂停与 sequencer 对接收资源的约束。
- ZooKeeper 3.9.2 recipes:协调锁配方;具体实现与 session/CAS 原码在组件锁、ID 与任务。znode version 不自动是跨恢复 fence。
- Raft 论文:持久任期/日志及安全与进展条件。本文模型不实现或实测 Raft。